package org.hackystat.devcathlon.engine.event.impl;

import javax.xml.datatype.XMLGregorianCalendar;
import org.hackystat.devcathlon.engine.event.Event;
import org.hackystat.devcathlon.engine.event.EventException;
import org.hackystat.devcathlon.engine.event.EventScore;
import org.hackystat.devcathlon.engine.match.Match;
import org.hackystat.devcathlon.engine.team.Team;
import org.hackystat.sensorbase.client.SensorBaseClient;
import org.hackystat.sensorbase.resource.projects.jaxb.Project;
import org.hackystat.sensorbase.resource.sensordata.jaxb.Property;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorData;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataIndex;
import org.hackystat.sensorbase.resource.sensordata.jaxb.SensorDataRef;
import org.hackystat.utilities.logger.HackystatLogger;
import org.hackystat.utilities.tstamp.Tstamp;

/**
 * Penalize any team that doesn't fix a broken build quickly.
 * <p>
 * Requires the following properties in the configuration.xml file for this event:
 * <ul>
 * <li>WakeupInterval: How frequently this event is invoked, and how far back it looks for data.
 * <li>BaseScoreWeight: Not used.
 * <li>NumberFailedBuildsMin: Minimum number of failed builds.
 * <li>NumberFailedBuildsMax: Maximum number of failed builds.
 * <li>NumberFailedBuildsPenalty: Penalty for failed builds.
 * <li>LargeNumberFailedBuilds: Large number of failed builds.
 * <li>LargeNumberFailedBuildsPenalty: Penalty for large number of failed builds.
 * </ul>
 * <p>
 * Specification:
 * <ul>
 * <li>Wakeup once every WakeupInterval.
 * <li>Retrieve all builds during that WakeupInterval.
 * <li>If there were more than NumberFailedBuildsMin but less than NumberFailedBuildsMax failing
 * Continuous Integration builds where
 * <ul>
 * <li>Type property is equal to "continuous.integration"
 * <li>Result property is equal to "Failure"
 * </ul>
 * deduct NumberFailedBuildsPenalty points.
 * <li>If there were more than LargeNumberFailedBuilds failing CI builds, deduct
 * LargeNumberFailedBuildsPenalty points.
 * <li>If there were more than LargeNumberFailedBuilds failing builds during the previous
 * WakeupInterval, then don't deduct any points for the current WakeupInterval.
 * </ul>
 * 
 * @author John Ancheta
 */
public class FixTheBuild extends Event {

  /** Support serialization. */
  private static final long serialVersionUID = 1L;

  /** The unique name for this event. */
  public static final String eventName = "Fix The Build";

  /** The unique description for this event. */
  public static final String description = "Penalize repeatedly failing CI builds.";

  /** Minimum number of failed builds. */
  private int numberFailedBuildsMin;

  /** Minimum number of failed builds key. */
  public static final String numberFailedBuildsMinKey = "NumberFailedBuildsMin";

  /** Minimum number of failed builds. */
  private int numberFailedBuildsMax;

  /** Minimum number of failed builds key. */
  public static final String numberFailedBuildsMaxKey = "NumberFailedBuildsMax";

  /** Penalty for failed builds. */
  private int numberFailedBuildsPenalty;

  /** Penalty for failed builds key. */
  public static final String numberFailedBuildsPenaltyKey = "NumberFailedBuildsPenalty";

  /** Large number of failed builds. */
  private int largeNumberFailedBuilds;

  /** Minimum number of failed builds key. */
  public static final String largeNumberFailedBuildsKey = "LargeNumberFailedBuilds";

  /** Penalty for large number of failed builds. */
  private int largeNumberFailedBuildsPenalty;

  /** Penalty for failed builds key. */
  public static final String largeNumberFailedBuildsPenaltyKey = "LargeNumberFailedBuildsPenalty";

  /**
   * Creates a new event for Fixing The Build. Ensures all required properties are available.
   * 
   * @throws EventException If the configuration cannot be created.
   */
  public FixTheBuild() throws EventException {
    super(eventName, description);
    try {
      numberFailedBuildsMin = Integer.parseInt(configManager.getProperty(eventName,
          numberFailedBuildsMinKey));
      numberFailedBuildsMax = Integer.parseInt(configManager.getProperty(eventName,
          numberFailedBuildsMaxKey));
      numberFailedBuildsPenalty = Integer.parseInt(configManager.getProperty(eventName,
          numberFailedBuildsPenaltyKey));
      largeNumberFailedBuilds = Integer.parseInt(configManager.getProperty(eventName,
          largeNumberFailedBuildsKey));
      largeNumberFailedBuildsPenalty = Integer.parseInt(configManager.getProperty(eventName,
          largeNumberFailedBuildsPenaltyKey));
    }
    catch (Exception e) {
      throw new EventException("Error configuring properties", e);
    }
  }

  /**
   * Returns the total number of points for this event.
   * 
   * @return The base score weight.
   */
  public int getBaseScoreWeight() {
    return baseScoreWeight;
  }

  /**
   * Returns the wakeup interval for this event.
   * 
   * @return The wakeup interval.
   */
  public int getWakeupInterval() {
    return wakeupInterval;
  }

  /**
   * Returns the minimum number of failed builds for this event.
   * 
   * @return The minimum number of failed builds.
   */
  public int getNumberFailedBuildsMin() {
    return numberFailedBuildsMin;
  }

  /**
   * Returns the maximum number of failed builds for this event.
   * 
   * @return The maximum number of failed builds.
   */
  public int getNumberFailedBuildsMax() {
    return numberFailedBuildsMax;
  }

  /**
   * Returns the penalty for number of failed builds for this event.
   * 
   * @return The penalty for number of failed builds.
   */
  public int getNumberFailedBuildsPenalty() {
    return numberFailedBuildsPenalty;
  }

  /**
   * Returns the large number of failed builds for this event.
   * 
   * @return The large number of failed builds.
   */
  public int getLargeNumberFailedBuilds() {
    return largeNumberFailedBuilds;
  }

  /**
   * Returns the penalty for large number of failed builds for this event.
   * 
   * @return The penalty for large number of failed builds.
   */
  public int getLargeNumberFailedBuildsPenalty() {
    return largeNumberFailedBuildsPenalty;
  }

  /**
   * Returns the total number of points for this event.
   * 
   * @param score The event score.
   * @return The total points for this event.
   */
  public int computePoints(EventScore score) {
    return score.getBaseScore();
  }

  /**
   * Creates an EventScore instance for FixTheBuild.
   * 
   * @param match The match of interest.
   * @param team The team of interest.
   * @param timestamp the time at which this event is being run.
   * @return The EventScore.
   * @throws EventException If problems occur while processing this event.
   */
  public EventScore computeScore(Match match, Team team, XMLGregorianCalendar timestamp)
      throws EventException {
    EventScore score = new EventScore(timestamp, match, team);

    Project project = match.getProject(team);
    SensorBaseClient client = match.getSensorBaseClient(team);

    String projectName = project.getName();
    String owner = project.getOwner();
    int consecutiveLargeNumberFailedBuilds = 0;
    XMLGregorianCalendar previousTimestamp = Tstamp
        .incrementMinutes(timestamp, wakeupInterval * -1);
    while (getTotalBuildFailures(client, owner, projectName, previousTimestamp) 
        >= largeNumberFailedBuilds) {
      consecutiveLargeNumberFailedBuilds++;
      previousTimestamp = Tstamp.incrementMinutes(previousTimestamp, wakeupInterval * -1);
    }
    if (consecutiveLargeNumberFailedBuilds % 2 != 0) {
      return score;
    }
    HackystatLogger.getLogger("org.hackystat.devcathlon", "devcathlon").info(
        String.format("Getting build sensor data for match %s and team %s", match.getName(), team
            .getName()));
    int currentTotalBuildFailures = getTotalBuildFailures(client, owner, projectName, timestamp);
    if (currentTotalBuildFailures >= largeNumberFailedBuilds) {
      score.setBaseScore(largeNumberFailedBuildsPenalty);
    }
    else if (currentTotalBuildFailures >= numberFailedBuildsMin) {
      score.setBaseScore(numberFailedBuildsPenalty);
    }

    // Finally return the final score.
    return score;
  }

  /**
   * Returns total number of build failures found in SensorDataIndex.
   * 
   * @param client The sensorbase client.
   * @param owner Owner.
   * @param projectName Project name.
   * @param time Time.
   * @return Total number of build failures.
   * @throws EventException An Exception.
   */
  private int getTotalBuildFailures(SensorBaseClient client, String owner, String projectName,
      XMLGregorianCalendar time) throws EventException {
    int totalBuildFailures = 0;

    try {
      SensorDataIndex index = client.getProjectSensorData(owner, projectName, Tstamp
          .incrementMinutes(time, wakeupInterval * -1), time, "Build");
      for (SensorDataRef ref : index.getSensorDataRef()) {
        SensorData data = client.getSensorData(ref);
        Property buildTypeProperty = data.findProperty("Type");
        Property buildResultProperty = data.findProperty("Result");
        if (buildTypeProperty != null
            && buildTypeProperty.getValue().equals("continuous.integration")
            && buildResultProperty != null && buildResultProperty.getValue().equals("Failure")) {
          totalBuildFailures++;
        }
      }
    }
    catch (Exception e) {
      throw new EventException("Error retrieving build data.", e);
    }

    return totalBuildFailures;
  }
}
